iT邦幫忙

2023 iThome 鐵人賽

DAY 7
0

https://ithelp.ithome.com.tw/upload/images/20230914/20119486X95TYtTszi.png

前言

前一篇我們嘗試用 Node.js 建立了 HTTP API,我相信你應該學到相當的多,但是實戰上來講,我們並不會直接使用 Node.js 來建立 HTTP API,因為這樣會太麻煩了,因此我們會使用一些框架來幫助我們建立 HTTP API,而這邊我們就來介紹一個非常熱門的框架,也就是 Express.js。

什麼是 Express.js?

什麼是 Express.js(又稱 Express) 呢?如果你本身具有前端開發經驗的話,你可以把它想像成是 Vue & Angular 為什麼呢?因為這兩個定位與 Express.js 非常相似,它們都是一個 Web 框架,只是 Vue & Angular 是用來開發網頁前端的,而 Express.js 則是專門針對 Node.js 的伺服器開發的框架。

Note
由於 React 在官方定義上是 Library,因此才沒有提到 React,但其實 Library 跟 Framework 的界線越來越模糊就是了。

Express 目前是廣泛被利用的框架,舉例來講...Paypal 與 Uber 就有使用到 Express.js 來開發伺服器,由此可知,我們可以知道 Express 是一個非常成熟的框架,因此我們可以放心的使用它來開發伺服器。

當然,除了 Express 還有其他替代品可以選擇,如:Koa.js、Fastify、NestJS 等等,但這一篇我們會比較著重於使用 Express 與介紹 Express 的相關知識,至於其他框架的部分...我們有緣再來介紹 :D

https://ithelp.ithome.com.tw/upload/images/20230914/20119486vslVUT1at8.png

建立專案

起手式請你依照以下指令來建立專案

mkdir express-example

接著請你進入專案中

cd express-example

請別忘了初始化專案

git init
npm init -y

接著請你安裝 Express

npm install express

Note
這邊我們使用 npm install express 來安裝 Express,但是你也可以使用 npm install express --save 來安裝,這兩個指令結果是相同的,因為 --save 是預設的,因此你可以省略;甚至你可以簡寫成 npm i express,因為 iinstall 的縮寫。

撰寫 Express

接下來我們要來建立一個我們的第一支檔案,也就是 index.js

touch index.js

建立好後你可能會想說我們第一行應該是 const http = require('node:http'); 對吧?但是這邊我們不會這樣做,因為我們要使用 Express,因此我們會使用 require('express') 來引入 Express

const express = require('express');

接著我們會需要將這個 Express 套件呼叫出來,因此我們會將 express() 呼叫出來,並且將它指派給 app 這個變數

const express = require('express');
const app = express();

接著我們會需要一個伺服器,因此我們會使用 app.listen() 來建立伺服器,並且指定 3000 這個 port,這邊我先順便補個簡單的 HTTP API,讓你可以在瀏覽器上看到結果

const express = require('express');
const app = express();

app.get('/', (req, res) => {
  res.send('Hello, World!');
});

app.listen(3000)

接下來你可以嘗試運行專案了,不意外你應該是可以在瀏覽器上看到「Hello, World!」。

「疑?就這麼簡單?」

沒錯,就是這個簡單,恭喜你體驗到 Web Framework 的威力了!我們這就是人家常在講的...

「站在巨人的肩膀上,你可以看得更遠。」

那麼我們可以相比一下我們前面寫的程式碼與使用 Express 後的程式碼

純 Node.js:

const http = require('node:http');

const hostname = '127.0.0.1';
const port = 3000;

const server = http.createServer((req, res) => {
  if (req.method === 'GET') {
    res.statusCode = 200;
    res.setHeader('Content-Type', 'application/json');
    res.end(JSON.stringify({ message: 'Hello, World!' }));
  }
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

使用 Express:

const express = require('express');
const app = express();

app.get('/', (req, res) => {
  res.send('Hello, World!');
});

app.listen(3000)

透過兩者之間的比較,你應該也可以深刻的體會到為什麼我們要使用 Express 了,畢竟它幫我們省去了很多的麻煩,讓我們可以專注在開發上,而不是在處理一些瑣碎的事情。

那麼這邊我也直接示範一下把前面章節我們所寫的 TodoList API 改成 Express 版本:

const express = require('express');
const app = express();
const port = 3000;

const data = [];

app.get('/todos', (req, res) => {
  res.send(data);
});

app.post('/todos', (req, res) => {
  const { title, completed } = req.body;

  data.push({
      id: new Date().getTime(),
      title,
      completed,
   });

  res.send(data);
});

app.listen(3000)

但是當你改成這樣後,去嘗試戳你會發現 Post /todos 會無法正常運作,這是因為我們需要一個東西來幫助我們解析 req.body,那該怎麼做呢?很簡單,補上 app.use(express.json()); 就可以了

const express = require('express');

const app = express();
const port = 3000;

const data = [];

app.use(express.json());

// ... 略過其他程式碼

app.listen(3000)

這樣子你在戳 Post /todos 就可以正常運作了。

Middleware

什麼是 Middleware 呢?你可以把它想像成中間人,以比較生活面的例子來講的話,你可以把它想像成你想要買房 or 租房,所以你會先經過仲介,然後仲介會幫你找到適合的房子,而這樣的過程就是中間層,而在程式碼上來講,你可以把它想像成一個函式,這個函式會在你的程式碼執行前,先執行這個函式,然後再執行你的程式碼,這樣的過程就是 Middleware。

房地產仲介幫助你找到適合的房子,就像 Middleware 在處理請求時,執行一些額外的操作,然後將處理權交給下一個處理或路由。

整個 Express 其實都是由 Middleware 的概念所組成的,因此 Middleware 是非常重要的一個概念。

回頭來講一下 app.use(express.json()); 這個 Middleware,前面有提到這個 Middleware 會幫助我們解析 req.body,讓我們可以正常取得資料,這邊讓我們來看一下它的原始碼:

// express/lib/express.js
var bodyParser = require('body-parser')
// ... 略過其他程式碼
exports.json = bodyParser.json

我們可以看到 app.use(express.json()); 其實就是 app.use(bodyParser.json());,只是被 Express 給封裝起來了而已,接著我們再往 body-parser 中看一下 json 的原始碼:

// body-parser/lib/types/json.js

// ... 略過其他程式碼

function json (options) {
  var opts = options || {}

  var limit = typeof opts.limit !== 'number'
    ? bytes.parse(opts.limit || '100kb')
    : opts.limit
  var inflate = opts.inflate !== false
  var reviver = opts.reviver
  var strict = opts.strict !== false
  var type = opts.type || 'application/json'
  var verify = opts.verify || false

  if (verify !== false && typeof verify !== 'function') {
    throw new TypeError('option verify must be function')
  }

  // create the appropriate type checking function
  var shouldParse = typeof type !== 'function'
    ? typeChecker(type)
    : type

  function parse (body) {
    if (body.length === 0) {
      // special-case empty json body, as it's a common client-side mistake
      // TODO: maybe make this configurable or part of "strict" option
      return {}
    }

    if (strict) {
      var first = firstchar(body)

      if (first !== '{' && first !== '[') {
        debug('strict violation')
        throw createStrictSyntaxError(body, first)
      }
    }

    try {
      debug('parse json')
      return JSON.parse(body, reviver)
    } catch (e) {
      throw normalizeJsonSyntaxError(e, {
        message: e.message,
        stack: e.stack
      })
    }
  }

  return function jsonParser (req, res, next) {
    if (req._body) {
      debug('body already parsed')
      next()
      return
    }

    req.body = req.body || {}

    // skip requests without bodies
    if (!typeis.hasBody(req)) {
      debug('skip empty body')
      next()
      return
    }

    debug('content-type %j', req.headers['content-type'])

    // determine if request should be parsed
    if (!shouldParse(req)) {
      debug('skip parsing')
      next()
      return
    }

    // assert charset per RFC 7159 sec 8.1
    var charset = getCharset(req) || 'utf-8'
    if (charset.slice(0, 4) !== 'utf-') {
      debug('invalid charset')
      next(createError(415, 'unsupported charset "' + charset.toUpperCase() + '"', {
        charset: charset,
        type: 'charset.unsupported'
      }))
      return
    }

    // read
    read(req, res, next, parse, debug, {
      encoding: charset,
      inflate: inflate,
      limit: limit,
      verify: verify
    })
  }
}

// ... 略過其他程式碼

這邊很複雜沒有錯,但真正整個核心是在 return function jsonParser (req, res, next) { ... } 這個函式,這個函式會在 req 之前執行,這過程中,就會依照設定來解析,例如:limitinflatereviverstricttypeverify 等等,當解析完畢後,就會將解析後的 JSON 資料附加到請求物件的 body 屬性中供後續路由處理函式使用。

看完一個超級複雜版本的 Middleware 後,我們來試著自己的 Middleware 吧?

自己的 Middleware

建立自己的 Middleware 其實並不困難,你只需要建立一個函式,並且在函式中呼叫 next() 就可以了

const express = require('express');

const app = express();

const myMiddleware = (req, res, next) => {
  console.log('myMiddleware!');
  next();
};

app.use(myMiddleware);

app.get('/', (req, res) => {
  res.send('Hello, World!');
});

app.listen(3000)

是不是很簡單呢?當你試著戳 http://localhost:3000 時,你會發現終端機中會印出 myMiddleware!

透過這個概念,以及總結前面的知識,其實我們是可以自己寫一個 JSON 的解析。

我們純 Node.js 的時候,其實是可以透過 req.on('data', (chunk) => { ... }) 來解析 JSON 的,因此我們可以透過這個概念來實作一個 JSON 解析的 Middleware

const express = require('express');

const app = express();

const jsonParserMiddleware = (req, res, next) => {
  let data = '';
  
  // 監聽數據流事件,並將數據流串接起來
  req.on('data', (chunk) => {
    data += chunk.toString();
  });

  // 監聽數據流結束事件,並將解析後的 JSON 資料附加到請求物件的 body 屬性
  req.on('end', () => {
    try {
      // 將解析後的 JSON 資料附加到請求物件的 body 屬性
      req.body = JSON.parse(data);
      
      // 繼續執行後續的 Middleware 或路由處理函式
      next();
    } catch (error) {
      // JSON 解析失敗,回傳錯誤訊息
      res.status(400).json({ error: 'Invalid JSON data' });
    }
  });
}

app.use(jsonParserMiddleware);

app.post('/', (req, res) => {
  res.send(req.body);
});

app.listen(3000)

透過自己所建立的 Middleware 我們就可以在 Post / 中取得 req.body 囉~

當然 Middleware 不只有可以做解析而已,在實戰上通常會搭配在身份驗證上,例如像是 Token 是否有過期、是否有權限等等,這些都可以透過 Middleware 來處理。

那麼這一篇也差不多要告一個段落了,所以最後我們就來總結一下 Middleware 的概念吧!

  • Middleware 是一個函式
  • Middleware 會在路由處理函式之前執行
  • Middleware 必須傳入 reqresnext 這三個參數
  • Middleware 必須呼叫 next() 才能繼續執行後續的動作
  • Middleware 可以用來做身份驗證、解析資料、處理錯誤等等

那麼這一篇我們就先到這邊結束囉~

碎碎念

前陣子使用 Nuxt3 的時候踩雷踩到快崩潰,還好 ChatGPT 救了我?!


上一篇
Day6 - 建立 HTTP API
下一篇
Day8 - 再續 Express.js
系列文
《Node.js 不負責系列:把前端人員當作後端來用,就算是前端也能嘗試寫的後端~原來 Node.js 可以做這麼多事~》31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言